﻿/************************************************************************

   Extended Silverlight Toolkit

   Copyright (C) 2010-2012 Xceed Software Inc.

   This program is provided to you under the terms of the Microsoft Public
   License (Ms-PL) as published at http://extendedsilverlight.codeplex.com/license

   Please purchase a commercial version of this toolkit if you are using
   it in a commercial product. The commercial versions support the project,
   and also include more features for each control, priority support,
   source code, updates, removed watermark, and a proprietary license.

   Visit http://xceed.com and follow @datagrid on Twitter.

  **********************************************************************/

using System;
using System.Diagnostics;
using System.Collections.Generic;
using System.Linq;
using Xceed.Silverlight.Data.Utils;

namespace Xceed.Silverlight.Data
{
  internal abstract class DataSourceEnumeratorBase : AsyncParentEnumerator<DataPath>, IAsyncDataSourceEnumerator
  {
    #region Static Fields

    private static readonly Guid ResetCallId = DataUtils.CreateCallId();
    private static readonly Guid GroupFetchCallId = DataUtils.CreateCallId();
    private static readonly double IndexAdjustmentRatio = ( 1d / 10d );

    #endregion Static Fields

    #region Constructor

    internal DataSourceEnumeratorBase(
      IAsyncEnumerator<DataPath> childEnumerator,
      IEnumerable<GroupCacheEnumeratorBase> groupCacheEnumerators,
      IPendingRequests pendingRequestSource,
      IGroupDataHelper dataSourceHelper,
      GroupHandler groupHandler )
      : base( childEnumerator )
    {
      if( dataSourceHelper == null )
        throw new ArgumentNullException( "dataSourceHelper" );

      if( groupHandler == null )
        throw new ArgumentNullException( "groupHandler" );

      if( groupCacheEnumerators != null )
      {
        m_groupCacheEnumerators = groupCacheEnumerators;
      }
      else
      {
        m_groupCacheEnumerators = Enumerable.Empty<GroupCacheEnumeratorBase>();
      }

      m_dataSourceHelper = dataSourceHelper;

      m_groupHandler = groupHandler;
      m_groupHandler.ExpansionChanging += new EventHandler<ExpansionChangedEventArgs>( this.OnExpansionChanging );

      m_pendingRequestSource = pendingRequestSource;
      if( m_pendingRequestSource != null )
      {
        m_pendingRequestSource.HasPendingRequestsChanged += new EventHandler( this.OnHasPendingRequestsChanged );
      }
    }

    #endregion

    #region Index Property

    public long Index
    {
      get
      {
        if( m_disposed )
          throw new ObjectDisposedException( "The enumerator has been disposed." );

        return
          ( m_currentOffset.ReferencePosition == ReferencePoint.BeforeFirst )
          ? m_currentOffset.Value - 1
          : m_itemCount.Value - m_currentOffset.Value;
      }
    }

    #endregion

    #region ItemCount Property

    public long ItemCount
    {
      get
      {
        if( m_disposed )
          throw new ObjectDisposedException( "The enumerator has been disposed." );

        return m_itemCount.Value;
      }
    }

    #endregion

    #region ItemCountEstimate Property

    protected EstimateItemCount ItemCountEstimate
    {
      get
      {
        return m_itemCount;
      }
    }

    #endregion

    #region CurrentOffset Property

    protected RelativeOffset CurrentOffset
    {
      get
      {
        return m_currentOffset;
      }
    }

    #endregion

    #region CurrentPosition Property

    internal SourcePosition CurrentPosition
    {
      get
      {
        return m_currentPosition;
      }
      private set
      {
        if( value == null )
          throw new ArgumentNullException( "CurrentPosition" );

        m_currentPosition = value;
      }
    }

    #endregion

    #region EnableApproximateMoves Property

    internal bool EnableApproximateMoves
    {
      get
      {
        return m_enableApproximateMoves;
      }

      set
      {
        m_enableApproximateMoves = value;
      }
    }

    private bool m_enableApproximateMoves = true;

    #endregion

    #region PreventEstimateIndex Property

    internal bool PreventEstimateIndex
    {
      get;
      set;
    }

    #endregion

    #region ProviderSupportGroupCount Property

    internal bool ProviderSupportGroupCount
    {
      get;
      set;
    }

    #endregion

    #region IndexChanged Event

    public event EventHandler<IndexChangedEventArgs> IndexChanged;

    protected virtual void RaiseIndexChanged( long oldValue, long newValue )
    {
      if( this.IndexChanged != null )
      {
        this.IndexChanged( this, new IndexChangedEventArgs( oldValue, newValue ) );
      }
    }

    #endregion

    #region ItemCountChanged Event

    public event EventHandler ItemCountChanged;

    protected virtual void RaiseItemCountChanged()
    {
      if( this.ItemCountChanged != null )
      {
        this.ItemCountChanged( this, EventArgs.Empty );
      }
    }

    #endregion

    #region Begin/End BaseJumpMoveAndFetch Methods

    protected JumpMoveAndFetchAsyncResult BeginBaseJumpMoveAndFetch(
      JumpMoveAndFetchParameters parameters,
      AsyncCallback userCallback, object asyncState )
    {
      return base.BeginJumpMoveAndFetch( parameters, userCallback, asyncState );
    }

    protected JumpMoveAndFetchResult<DataPath> EndBaseJumpMoveAndFetch(
      JumpMoveAndFetchAsyncResult asyncResult )
    {
      return base.EndJumpMoveAndFetch( asyncResult );
    }

    #endregion

    #region Begin/End JumpMoveAndFetch

    public override JumpMoveAndFetchResult<DataPath> EndJumpMoveAndFetch(
      JumpMoveAndFetchAsyncResult asyncResult )
    {
      JumpMoveAndFetchResult<DataPath> result = base.EndJumpMoveAndFetch( asyncResult );

      SourcePosition initialCurrentPosition = m_currentPosition;
      m_currentPosition = result.EndPositionObject;

      int compareResult = m_dataSourceHelper.Compare( initialCurrentPosition, result.SourcePosition );
      FetchStartPosition operationStartPosition = FetchStartPosition.SameAsInitial;

      if( compareResult < 0 )
      {
        operationStartPosition = FetchStartPosition.After;
      }
      else if( compareResult > 0 )
      {
        operationStartPosition = FetchStartPosition.Before;
      }


      HandleDisplacement(
        operationStartPosition,
        result.GetTotalDisplacement(),
        this.HandleUnsupportedEstimateIndex(result.EndPositionOffset),
        result.TotalCount );

      this.AdjustIndexRelativePosition();

      return result;
    }

    #endregion

    #region Begin/End FetchGroup Methods

    public IAsyncResult BeginFetchGroup(
      DataPath groupPath,
      GroupTarget groupTarget,
      int expectedFetchCount,
      AsyncCallback userCallback,
      object asyncState )
    {
      if( expectedFetchCount == 0 )
        throw new ArgumentException( "expectedFetchCount must be less than or greater than zero.", "expectedFetchCount" );

      ContextAsyncResult parentAsyncResult = new ContextAsyncResult(
        DataSourceEnumeratorBase.GroupFetchCallId,
        userCallback, asyncState );

      parentAsyncResult.CallContext = new Tuple<int, GroupTarget>( expectedFetchCount, groupTarget );

      bool twoFetchOpration = ( ( groupTarget == GroupTarget.Previous ) && ( expectedFetchCount > 0 ) )
        || ( ( groupTarget == GroupTarget.Next ) && ( expectedFetchCount < 0 ) );

      StartPosition fetchStartPosition =
        ( groupTarget == GroupTarget.Next ) || ( ( groupTarget == GroupTarget.Current ) && ( expectedFetchCount < 0 ) )
        ? StartPosition.After
        : StartPosition.Before;

      if( !twoFetchOpration )
      {
        JumpMoveAndFetchParameters parameters = new JumpMoveAndFetchParameters(
          this.CreateGroupFetchSourcePosition( groupPath, fetchStartPosition ),
          0,
          expectedFetchCount,
          false );

        Func<AsyncCallback, object, IAsyncResult> fetchCall =
          ( callback, state ) => this.BeginBaseJumpMoveAndFetch( parameters, callback, state );

        Func<AsyncResult, IAsyncResult, DependentCallResult> fetchCallback =
          ( parentAr, ar ) =>
          {
            JumpMoveAndFetchResult<DataPath> result = this.EndBaseJumpMoveAndFetch( ( JumpMoveAndFetchAsyncResult )ar );
            GroupFetchResult groupFetchResult = new GroupFetchResult(
              result.FetchedItems, result.EndPositionObject,
              result.EndPositionOffset, result.TotalCount );

            return new DependentCallResult( groupFetchResult );
          };

        parentAsyncResult.Start( fetchCall, fetchCallback );
      }
      else
      {

        JumpMoveAndFetchParameters firstCallParams;
        JumpMoveAndFetchParameters secondCallParams;
        Func<AsyncCallback, object, IAsyncResult> firstCall;
        Func<AsyncCallback, object, IAsyncResult> secondCall;
        Func<AsyncResult, IAsyncResult, DependentCallResult> firstCallback;
        Func<AsyncResult, IAsyncResult, DependentCallResult> secondCallback;

        firstCallParams = new JumpMoveAndFetchParameters(
          this.CreateGroupFetchSourcePosition( groupPath, fetchStartPosition ),
          0, -DataUtils.GetUnit( expectedFetchCount ), false );

        firstCall = ( callback, state ) =>
            this.BeginBaseJumpMoveAndFetch( firstCallParams, callback, state );

        secondCallback = ( parentAr, ar ) =>
        {
          JumpMoveAndFetchResult<DataPath> result = this.EndBaseJumpMoveAndFetch( ( JumpMoveAndFetchAsyncResult )ar );

          JumpMoveAndFetchResult<DataPath> firstCallResult = ar.AsyncState as JumpMoveAndFetchResult<DataPath>;

          if( firstCallResult == null )
            throw new DataInternalException();

          EstimateInt64 totalCount =
            ( result.TotalCount != null )
            ? result.TotalCount
            : firstCallResult.TotalCount.ToEstimate();

          GroupFetchResult groupFetchResult = new GroupFetchResult(
            result.FetchedItems, result.EndPositionObject, result.EndPositionOffset, totalCount );

          return new DependentCallResult( groupFetchResult );
        };

        firstCallback = ( parentAr, ar ) =>
        {
          JumpMoveAndFetchResult<DataPath> adjacentItem = this.EndBaseJumpMoveAndFetch( ( JumpMoveAndFetchAsyncResult )ar );
          if( adjacentItem.FetchedItems == null )
          {
            GroupFetchResult groupFetchResult = new GroupFetchResult(
              null, adjacentItem.EndPositionObject,
              adjacentItem.EndPositionOffset, adjacentItem.TotalCount );

            return new DependentCallResult( groupFetchResult );
          }
          else
          {
            DataPath adjacentGroupPath = adjacentItem.FetchedItems.First();
            if( adjacentGroupPath.Depth > groupPath.Depth )
            {
              adjacentGroupPath = adjacentGroupPath.CreateAncestorPath( groupPath.Depth );
            }

            secondCallParams = new JumpMoveAndFetchParameters(
              this.CreateGroupFetchSourcePosition( adjacentGroupPath, fetchStartPosition ),
              0, expectedFetchCount, false );

            secondCall = ( callback, state ) =>
              this.BeginBaseJumpMoveAndFetch( ( JumpMoveAndFetchParameters )secondCallParams, callback, state );

            return new DependentCallResult( adjacentItem, secondCall, secondCallback );
          }
        };

        parentAsyncResult.Start( firstCall, firstCallback );
      }

      return parentAsyncResult;
    }

    public GroupFetchResult EndFetchGroup( IAsyncResult asyncResult )
    {
      asyncResult.ValidateCallEnd( DataSourceEnumeratorBase.GroupFetchCallId );

      GroupFetchResult result = asyncResult.GetCallResult<GroupFetchResult>();

      Tuple<int, GroupTarget> callContext = asyncResult.GetCallContext<Tuple<int, GroupTarget>>();

      int expectedFetchCount = callContext.Item1;
      GroupTarget groupTarget = callContext.Item2;

      m_currentPosition = result.EndPositionObject;

      FetchStartPosition operationStartPosition;

      if( groupTarget == GroupTarget.Previous )
      {
        operationStartPosition = FetchStartPosition.Before;
      }
      else if( groupTarget == GroupTarget.Next )
      {
        operationStartPosition = FetchStartPosition.After;
      }
      else //(groupTarget == GroupTarget.Current)
      {
        Debug.Assert( groupTarget == GroupTarget.Current );

        //StartPosition is "before" if we fetch the group top,
        //and "after" if we fetched the group end.
        if( expectedFetchCount < 0 )
        {
          operationStartPosition = FetchStartPosition.After;
        }
        else
        {
          operationStartPosition = FetchStartPosition.Before;
        }
      }

      HandleDisplacement(
        operationStartPosition,
        result.GetFetchDisplacement( expectedFetchCount ),
        this.HandleUnsupportedEstimateIndex( result.EndPositionOffset ),
        result.TotalCount );

      this.AdjustIndexRelativePosition();

      return result;
    }

    public virtual SourcePosition CreateGroupFetchSourcePosition( DataPath groupPath, StartPosition fetchStartPosition )
    {
      return m_dataSourceHelper.CreateSortPosition( groupPath, fetchStartPosition );
    }

    #endregion

    #region Begin/End MoveAndFetch

    public IMoveAndFetchAsyncResult BeginMoveAndFetch(
      bool approximateMove,
      long expectedMoveCount,
      int expectedFetchCount,
      AsyncCallback userCallback,
      object asyncState )
    {
      approximateMove = approximateMove && this.EnableApproximateMoves;

      JumpMoveAndFetchParameters parameters = new JumpMoveAndFetchParameters(
                                                    m_currentPosition,
                                                    expectedMoveCount,
                                                    expectedFetchCount,
                                                    approximateMove );

      return this.BeginJumpMoveAndFetch(
               parameters,
               userCallback,
               asyncState );
    }

    public MoveAndFetchResult<DataPath> EndMoveAndFetch(
      IMoveAndFetchAsyncResult asyncResult )
    {
      JumpMoveAndFetchAsyncResult jumpAsyncResult = ( JumpMoveAndFetchAsyncResult )asyncResult;
      JumpMoveAndFetchResult<DataPath> result = this.EndJumpMoveAndFetch( jumpAsyncResult );

      return new MoveAndFetchResult<DataPath>(
                   result.ActualMoveCount,
                   result.FetchedItems,
                   result.ApproximateMove,
                   result.ExpectedMoveCount,
                   result.ExpectedFetchCount,
                   result.Result,
                   result.EndPositionOffset,
                   result.TotalCount );
    }

    #endregion

    #region Begin/End Reset

    public IAsyncResult BeginReset( AsyncCallback userCallback, object asyncState )
    {
      AsyncResult asyncResult = new AsyncResult( DataSourceEnumeratorBase.ResetCallId, userCallback, asyncState );
      asyncResult.StartSync( new ResetResult() );
      return asyncResult;

    }

    public ResetResult EndReset( IAsyncResult resetAsyncResult )
    {
      resetAsyncResult.ValidateCallEnd( DataSourceEnumeratorBase.ResetCallId );
      m_currentPosition = SourcePosition.BeforeFirst;
      return resetAsyncResult.GetCallResult<ResetResult>();
    }

    #endregion

    internal RelativeOffset HandleUnsupportedEstimateIndex( RelativeOffset endPositionOffset )
    {
      if( this.PreventEstimateIndex && endPositionOffset != null && endPositionOffset.IsEstimate )
      {
        bool estimateByHeadersFooters = ( !this.ProviderSupportGroupCount && m_groupHandler.AnyHeaderFooter );
        bool estimateByCollapsedGroups = ( ( m_groupHandler.GroupCountPolicy == GroupCountPolicy.DoNotHandleCounts ) && m_groupHandler.AnyCollapsedGroup );

        if( estimateByCollapsedGroups || estimateByHeadersFooters )
        {
          endPositionOffset = null;
        }
      }

      return endPositionOffset;
    }


    public virtual bool IsFirst( DataPath path, int levelDepth )
    {
      if( path == null )
        throw new ArgumentNullException( "path" );

      if( levelDepth < 0 || levelDepth >= path.Depth )
        throw new ArgumentOutOfRangeException( "levelDepth", "levelDepth must be greater than or equal to zero and less than the path depth." );

      if( !this.IsFullySynchronized )
        throw new InvalidOperationException( "An attempt was made to use IsFirst while the enumerator is not fully synchronized." );

      JumpMoveAndFetchParameters parameters;

      if( m_dataSourceHelper.IsGroupPath( path ) && !m_groupHandler.IsCollapsed( path ) )
      {
        parameters = new JumpMoveAndFetchParameters(
          m_dataSourceHelper.CreateSortPosition( path, StartPosition.Before ),
          0,
          -1 );
      }
      else
      {
        parameters = new JumpMoveAndFetchParameters(
          new ItemPosition( path ),
          -1,
          -1 );
      }




      JumpMoveAndFetchAsyncResult asyncResult = this.ChildEnumeratorTyped.BeginJumpMoveAndFetch(
                                                parameters,
                                                null,
                                                null ).EnsureSyncness();

      JumpMoveAndFetchResult<DataPath> result = this.ChildEnumeratorTyped.EndJumpMoveAndFetch( asyncResult );

      if( result.FetchedItems == null )
      {
        return true;
      }

      if( levelDepth == 0 )
      {
        return false;
      }

      DataPath previousPath = result.FetchedItems.First();
      bool isFirst = false;

      for( int i = 1; i <= levelDepth; i++ )
      {
        if( previousPath.Depth < i )
        {
          isFirst = true;
          break;
        }

        if( !object.Equals( previousPath[ i - 1 ], path[ i - 1 ] ) )
        {
          isFirst = true;
          break;
        }
      }

      return isFirst;
    }

    protected virtual void ExpansionChanging( ExpansionChangedEventArgs e )
    {
      foreach( GroupCacheEnumeratorBase cacheEnumerator in m_groupCacheEnumerators )
      {
        cacheEnumerator.ExpansionChanging( e.Action, e.Path );
      }

      ItemPosition currentPosition = this.CurrentPosition as ItemPosition;

      if( currentPosition != null )
      {
        DataPath path = ( DataPath )currentPosition.Item;

        if( e.Action == ExpansionAction.CollapseAll || e.Action == ExpansionAction.Collapse )
        {
          DataPath newCollapsedCurrent = null;
          if( e.Action == ExpansionAction.Collapse )
          {
            if( e.Path.IsAncestorOf( path ) )
            {
              newCollapsedCurrent = e.Path;
            }
          }
          else 
          {
            DataPath groupingPath = m_dataSourceHelper.GetGroupPath( path );

            if( e.ExpansionDepth <= groupingPath.Depth )
            {
              newCollapsedCurrent = path.CreateAncestorOrSamePath( e.ExpansionDepth );
            }
          }

          if( newCollapsedCurrent != null )
          {
            //The current is an item of the newly collapsed group.
            //Set it at the head of the group so we can see the collapsed header of the group.
            //Otherwise, the current would be skipped directly after the collapsed group.
            this.CurrentPosition = new ItemPosition( newCollapsedCurrent );
          }
        }
        else
        {
          Debug.Assert( e.Action == ExpansionAction.Expand || e.Action == ExpansionAction.ExpandAll );

          DataPath groupingPath = m_dataSourceHelper.GetGroupPath( path );

          if( groupingPath.Depth == path.Depth )
          {
            if( ( ( e.Action == ExpansionAction.Expand ) && e.Path.Equals( path ) )
              || ( ( e.Action == ExpansionAction.ExpandAll ) && ( groupingPath.Depth == e.ExpansionDepth ) ) )
            {
              this.CurrentPosition = m_dataSourceHelper.CreateSortPosition( path, StartPosition.Before );
            }
          }
        }
      }
    }

    protected override void ChildDataChanged( DataChangedEventArgs args )
    {
      if( m_disposed )
        throw new ObjectDisposedException( "The enumerator has been disposed." );

      if( args.Action == DataChangedAction.Replace )
      {
        this.HandleCurrentReplace( ( DataPath )args.OldItem, ( DataPath )args.NewItem );
      }
      else if( args.Action == DataChangedAction.Add )
      {
        RelativeOffset newOffset = m_currentOffset;

        if( m_dataSourceHelper.Compare( m_currentPosition, ( DataPath )args.NewItem ) > 0 )
        {
          newOffset = DataUtils.CalculateRelativeOffset( m_currentOffset, 1 );
        }

        this.SetCountValues( newOffset, new EstimateItemCount( m_itemCount.Value + 1, m_itemCount.IsEstimate, m_itemCount.IsBasedOnOffset ) );
      }
      else if( args.Action == DataChangedAction.Remove )
      {
        RelativeOffset dataSourceOffset;
        RelativeOffset calculatedOffset;
        SourcePosition newCurrentPosition;

        this.HandleCurrentRemove( ( DataPath )args.OldItem, args.RemovePosition, out newCurrentPosition, out dataSourceOffset );

        if( m_dataSourceHelper.Compare( m_currentPosition, ( DataPath )args.OldItem ) > 0 )
        {
          calculatedOffset = DataUtils.CalculateRelativeOffset( m_currentOffset, -1 );
        }
        else
        {
          calculatedOffset = m_currentOffset;
        }

        RelativeOffset newOffset = ( dataSourceOffset != null ) ? dataSourceOffset : calculatedOffset;
        EstimateItemCount newCount = new EstimateItemCount( m_itemCount.Value - 1, m_itemCount.IsEstimate, m_itemCount.IsBasedOnOffset );

        if( dataSourceOffset != null && newCount.IsBasedOnOffset )
        {
          newCount = DataSourceEnumeratorBase.CalculateNewCountBasedOnOffsets( calculatedOffset, dataSourceOffset, newCount );
        }

        this.SetCountValues( newOffset, newCount );

        args = new DataChangedEventArgs(
             DataChangedAction.Remove,
             args.OldItem,
             args.RemovePosition,
             newCurrentPosition );
      }

      base.ChildDataChanged( args );
    }

    protected bool HandleCurrentRemove(
      DataPath removed,
      SourcePosition removePosition,
      out SourcePosition newCurrentPosition,
      out RelativeOffset newCurrentOffset )
    {
      if( removed == null )
        throw new ArgumentNullException( "removed" );

      newCurrentPosition = null;
      newCurrentOffset = null;

      bool isCurrentItem = false;
      SourcePosition currentSourcePosition = m_currentPosition;

      // Figure out if the removed item is the current item.
      if( currentSourcePosition is DummyPosition<DataPath> )
      {
        DummyPosition<DataPath> dummyPosition = ( DummyPosition<DataPath> )currentSourcePosition;

        if( dummyPosition.BasedOnItem )
        {
          isCurrentItem = object.Equals( dummyPosition.BaseItem, removed );
        }
        else
        {
          currentSourcePosition = dummyPosition.BaseSourcePosition;
        }
      }

      if( currentSourcePosition is ItemPosition )
      {
        ItemPosition itemPosition = ( ItemPosition )currentSourcePosition;

        isCurrentItem = object.Equals( itemPosition.Item, removed );
      }

      // The current item has been removed.  We must replace it by a valid SourcePosition.
      if( isCurrentItem )
      {
        if( removePosition == SourcePosition.BeforeFirst )
        {
          m_currentPosition = SourcePosition.BeforeFirst;
          newCurrentPosition = SourcePosition.BeforeFirst;
          newCurrentOffset = new RelativeOffset( 0, ReferencePoint.BeforeFirst );
        }
        else if( removePosition == SourcePosition.AfterLast )
        {
          m_currentPosition = SourcePosition.AfterLast;
          newCurrentPosition = SourcePosition.AfterLast;
          newCurrentOffset = new RelativeOffset( 0, ReferencePoint.AfterLast );
        }
        else
        {
          Debug.Assert( removePosition == null );
          //If the current is the deleted item, the next fetch will take
          //the previous or following items in the source.
        }
      }

      return ( newCurrentPosition != null );
    }

    protected virtual bool HandleCurrentReplace( DataPath oldItem, DataPath newItem )
    {
      DataPath currentDataPath;
      if( !this.TryGetCurrentDataPath( out currentDataPath ) )
        return false;

      bool isSameDataPath = object.Equals( currentDataPath, oldItem );
      if( isSameDataPath )
      {
        Debug.Assert( newItem != null );
        m_currentPosition = new ItemPosition( newItem );
      }

      return isSameDataPath;
    }

    private bool TryGetCurrentDataPath( out DataPath dataPath )
    {
      dataPath = null;

      ItemPosition itemPosition = m_currentPosition as ItemPosition;
      if( itemPosition != null )
      {
        dataPath = itemPosition.Item as DataPath;
      }

      DummyPosition<DataPath> dummyPosition = m_currentPosition as DummyPosition<DataPath>;
      if( ( dummyPosition != null ) && ( dummyPosition.BasedOnItem ) )
      {
        dataPath = dummyPosition.BaseItem;
      }

      return ( dataPath != null );
    }

    private void OnExpansionChanging( object sender, ExpansionChangedEventArgs e )
    {
      this.ExpansionChanging( e );
    }

    #region Index and Count Management

    internal void SetCountValues( RelativeOffset newOffset, EstimateItemCount newCount )
    {
      long oldIndex = this.Index;
      long oldCount = this.ItemCount;

      if( newOffset != null )
      {
        m_currentOffset = newOffset;
      }

      if( newCount != null )
      {
        m_itemCount = newCount;
      }

      if( oldCount != this.ItemCount )
      {
        this.RaiseItemCountChanged();
      }

      if( oldIndex != this.Index )
      {
        this.RaiseIndexChanged( oldIndex, this.Index );
      }
    }

    private void HandleDisplacement(
      FetchStartPosition fetchStartPosition,
      EstimateInt64 totalDisplacement,
      RelativeOffset limitOffset,
      EstimateInt64 totalCount )
    {

      Debug.Assert( totalDisplacement != null || ( limitOffset != null && !limitOffset.IsEstimate ),
        "If total displacement is null, the move should have failed, hence having a precise position" );

      RelativeOffset newOffset;
      RelativeOffset calculatedOffset = null;
      RelativeOffset dataSourceOffset = null;

      EstimateItemCount newCount;

      if( totalCount != null )
      {
        newCount = new EstimateItemCount( totalCount, false );
      }
      else
      {
        newCount = new EstimateItemCount( this.ItemCount, true, m_itemCount.IsBasedOnOffset );
      }

      if( limitOffset != null && !limitOffset.IsEstimate )
      {
        dataSourceOffset = limitOffset;
      }


      if( dataSourceOffset == null || newCount.IsBasedOnOffset )
      {
        RelativeOffset initialOffset = m_currentOffset.ToEstimate();

        if( fetchStartPosition == FetchStartPosition.After )
        {
          initialOffset = DataUtils.CalculateRelativeOffset( initialOffset, ( this.ItemCount - this.Index ) / 2 );
        }
        else if( fetchStartPosition == FetchStartPosition.Before )
        {
          double adjustment = Math.Floor( ( ( double )this.Index ) / 2 );
          initialOffset = DataUtils.CalculateRelativeOffset( initialOffset, -Convert.ToInt64( adjustment ) );
        }

        calculatedOffset = DataUtils.CalculateRelativeOffset( initialOffset, totalDisplacement.Value, false );

        if( dataSourceOffset != null && newCount.IsBasedOnOffset )
        {
          newCount = DataSourceEnumeratorBase.CalculateNewCountBasedOnOffsets( calculatedOffset, dataSourceOffset, newCount );
        }
      }

      newOffset = ( dataSourceOffset != null ) ? dataSourceOffset : calculatedOffset;

      Debug.Assert( newOffset != null );

      if( !this.IsCountValuesValid( newOffset, newCount ) )
      {
        this.HandleInconsistentCountData( ref newOffset, ref newCount );

        if( !this.IsCountValuesValid( newOffset, newCount ) )
        {
          throw new DataInternalException(); //The data-count information is invalid.
        }
      }

      if( newOffset.IsEstimate && newOffset.Value < 1 )
      {
        long adjustment = ( 1 - newOffset.Value );
        newOffset += adjustment;

        if( newCount.IsBasedOnOffset )
        {
          Debug.Assert( newCount.IsEstimate );
          newCount = new EstimateItemCount( newCount.Value + adjustment, true, true );
        }
      }

      if( newOffset.Value > newCount.Value )
      {
        if( ( !newOffset.IsEstimate || newCount.IsBasedOnOffset ) && newCount.IsEstimate )
        {
          newCount = new EstimateItemCount( newOffset.Value, true, newCount.IsBasedOnOffset );
        }
        else if( newOffset.IsEstimate )
        {
          newOffset = new RelativeOffset( new EstimateInt64( newCount.Value, true ), newOffset.ReferencePosition );
        }
      }

      this.SetCountValues( newOffset, newCount );
    }


    protected virtual void HandleInconsistentCountData( ref RelativeOffset currentOffset, ref EstimateItemCount itemCount )
    {
      throw new DataInternalException(); //The data-count information is invalid.
    }

    protected static EstimateItemCount CalculateNewCountBasedOnOffsets( RelativeOffset expectedOffset, RelativeOffset providedOffset, EstimateItemCount count )
    {
      if( expectedOffset == null )
        throw new ArgumentNullException( "expectedOffset" );

      if( providedOffset == null )
        throw new ArgumentNullException( "providedOffset" );

      if( count == null )
        throw new ArgumentNullException( "count" );

      if( !count.IsBasedOnOffset )
        throw new DataInternalException(); //A new count with a more precise item count was received.

      EstimateItemCount newCount;

      if( expectedOffset.ReferencePosition == providedOffset.ReferencePosition )
      {
        long diff = expectedOffset.Value - providedOffset.Value;
        newCount = new EstimateItemCount( Math.Max( 0, count.Value - diff ), true, true );
      }
      else
      {
        long total = expectedOffset.Value + providedOffset.Value;
        newCount = new EstimateItemCount( total, true, true );
      }

      return newCount;
    }

    protected virtual void AdjustIndexRelativePosition()
    {
      if( !m_itemCount.IsEstimate && !m_currentOffset.IsEstimate )
        return;



      RelativeOffset newOffset = m_currentOffset;
      EstimateItemCount newCount = m_itemCount;
      Func<long, long> itemCountAdjustment = null;
      Func<long, long> indexAdjustment = null;


      itemCountAdjustment = ( count ) => Convert.ToInt64( Math.Ceiling(
        ( DataSourceEnumeratorBase.IndexAdjustmentRatio * count ) /
        ( 1 - DataSourceEnumeratorBase.IndexAdjustmentRatio )
        ) );

      indexAdjustment = ( count ) => Convert.ToInt64( Math.Ceiling(
          ( ( double )count ) * DataSourceEnumeratorBase.IndexAdjustmentRatio
          ) );



      long topOffsetAdjustmentLimit =
        ( newCount.IsBasedOnOffset )
        ? newCount.Value - Math.Max( 1, indexAdjustment( newCount.Value ) / 2 )
        : newCount.Value;

      if( newCount.IsEstimate && ( newCount.IsBasedOnOffset || !newOffset.IsEstimate ) )
      {
        if( newOffset.IsEstimate && newOffset.Value <= 1 )
        {
          newCount = new EstimateItemCount( newCount.Value + itemCountAdjustment( newCount.Value ), newCount.IsEstimate, newCount.IsBasedOnOffset );
        }
        else if( newOffset.Value > topOffsetAdjustmentLimit )
        {
          newCount = new EstimateItemCount( newOffset.EstimateValue.Value + itemCountAdjustment( newOffset.EstimateValue.Value ), true, newCount.IsBasedOnOffset );
        }
      }

      if( newOffset.IsEstimate )
      {
        if( newOffset.Value <= 1 )
        {
          newOffset = new RelativeOffset(
            new EstimateInt64( indexAdjustment( newCount.Value ), true ),
            m_currentOffset.ReferencePosition );
        }
        else if( newOffset.Value > topOffsetAdjustmentLimit )
        {
          newOffset = new RelativeOffset(
            new EstimateInt64( newCount.Value - indexAdjustment( newCount.Value ), true ),
            m_currentOffset.ReferencePosition );
        }
      }

      if( newOffset.IsEstimate && newCount.Value > 0
        && ( newOffset.Value <= 1 || newOffset.Value >= newCount.Value ) )
      {
        newOffset = new RelativeOffset(
          Math.Max( 1, Math.Min( newCount.Value - 1, newOffset.Value ) ),
          newOffset.ReferencePosition );
      }

      this.SetCountValues( newOffset, newCount );
    }

    private bool IsCountValuesValid( RelativeOffset currentOffset, EstimateItemCount itemCount )
    {
      bool currentOffsetIsNegativeError = false;
      bool currentOffsetIsToBigError = false;

      if( currentOffset != null && !currentOffset.IsEstimate )
      {
        currentOffsetIsNegativeError = ( currentOffset.Value < 0 );

        if( itemCount != null && !itemCount.IsEstimate )
        {
          currentOffsetIsToBigError = ( currentOffset.Value > itemCount.Value + 1 );
        }
      }

      return !currentOffsetIsNegativeError && !currentOffsetIsToBigError;
    }

    #endregion Index and Count  Management

    #region IPendingRequests Members

    public bool HasPendingRequests
    {
      get
      {
        if( m_pendingRequestSource == null )
          return false;

        return m_pendingRequestSource.HasPendingRequests;
      }
    }

    public event EventHandler HasPendingRequestsChanged;

    protected virtual void RaiseHasPendingRequestsChanged()
    {
      if( this.HasPendingRequestsChanged != null )
      {
        this.HasPendingRequestsChanged( this, EventArgs.Empty );
      }
    }

    private void OnHasPendingRequestsChanged( object sender, EventArgs e )
    {
      this.RaiseHasPendingRequestsChanged();
    }

    #endregion

    #region IDisposable Members

    protected override void Dispose( bool disposing )
    {
      m_groupHandler.ExpansionChanging -= new EventHandler<ExpansionChangedEventArgs>( this.OnExpansionChanging );

      if( m_pendingRequestSource != null )
      {
        m_pendingRequestSource.HasPendingRequestsChanged -= new EventHandler( this.OnHasPendingRequestsChanged );
      }

      m_disposed = true;

      base.Dispose( disposing );
    }

    private bool m_disposed; //false

    #endregion

    #region Private Fields

    private EstimateItemCount m_itemCount = new EstimateItemCount( 0, true, true );
    private RelativeOffset m_currentOffset = new RelativeOffset( 0, ReferencePoint.BeforeFirst );
    private SourcePosition m_currentPosition = SourcePosition.BeforeFirst;

    private readonly IGroupDataHelper m_dataSourceHelper;
    private readonly IPendingRequests m_pendingRequestSource; //null
    private readonly GroupHandler m_groupHandler;
    private readonly IEnumerable<GroupCacheEnumeratorBase> m_groupCacheEnumerators;

    #endregion

    #region FetchStartPosition Nested Type

    private enum FetchStartPosition
    {
      SameAsInitial,
      Before,
      After
    }

    #endregion
  }
}
